#!/usr/bin/python

#######################################################################################################
##Program that analyzes the linperf artifacts and finds the top CPU consuming Threads of IBM JVM
##Requirements: Python version greater than 2.6
##Usage: The tool should be run in the same directory with linperf.sh, preferably as root.
##        It will by default print the top 5 CPU consuming java threads together with their stack trace 
#######################################################################################################
import tarfile
import re
import operator
import glob
import os
import shutil
import sys
import platform
from tarfile import TarError

########################################################################################################
#class WildThread
#Describes a thread
########################################################################################################
class WildThread:
    def __init__(self, tid, rate, details=None):
        self.tid = tid
        self.rate = rate
        self.details = details        
        

    def printWildThread(self):
        """Print all thread information"""
        print 'TID:{0}'.format(self.tid)
        print 'Thread Rate(Average CPU Usage):{0}%'.format(self.rate)
        if self.details != None:
            print 'Thread Stack Trace:{0}'.format(self.details)

########################################################################################################
#class LinperfFileHandler
#Handles the linperf output files
########################################################################################################
class LinperfFileHandler:
    ZNAME = 'linperf_RESULTS.tar.gz'
    WASPROFHOME = '/opt/ibm/websphere/appserver/profiles/AppSrv01'
    topdashhfile = ''
    @classmethod    
    def readtopDashH(cls):
        """Extract and read the topdashH file from linperf compr file"""
        try:
            tar = tarfile.open(cls.ZNAME)
        except IOError:
            raise IOError('Error opening the tar file...')
        
        namelist = tar.getnames()
        for x in namelist:
            topdashhfile = ''
            if x.startswith('topdashH'):
                topdashhfile = x
        try:
            tar.extract(topdashhfile)
        except TarError:
            raise TarError('Error extracting the topdashH file from the tar file...')
        
        try:
            topHfile = open(topdashhfile)
            print 'Reading top -H Output...'
            topHtxt = topHfile.read()
        except IOError:
            raise IOError('Error opening the topdashH file...')
        finally:
            topHfile.close()
        
        return topHtxt

    @classmethod
    def MoveJavacoresHere(cls):
        """Move Javacores to current dir(i.e. where linperf lies)"""
        if platform.system() == 'Linux':
            while not os.path.exists(cls.WASPROFHOME): 
                    print 'Could not find the Javacores...'
                    cls.WASPROFHOME = raw_input('Please provide the WAS Profile under which linperf ran:')
                    
            jclist = glob.glob(cls.WASPROFHOME + '/javacore.*')
            print 'Moving Javacores in current directory...'
            for jc in jclist:
                shutil.move(jc, '.')

########################################################################################################
#class ThreadAnalysis
#Performs the Thread Analysis
########################################################################################################
class ThreadAnalysis:
    TopN = 5
    WildThreadList = []
    JAVAPLATFORM = []
    
    @classmethod
    def RateJavaThreadsPerformance(cls, topHtxt):
        '''Read the topdashH file and find the topN threads with highest CPU consumption'''
        reiter = re.finditer('^(\d+)\s+\S+\s+\S+\s+\S+\s+\S+\s+\S+\s+\S+\s+\S+\s+(\d+)\s+.+$', topHtxt, re.MULTILINE)
        D = {}
        
        #Sum the CPU usage for each thread found in topdashH file
        for m in reiter:
            if m.group(1) not in D:
                D[m.group(1)] = int(m.group(2))
            else:
                D[m.group(1)] = D[m.group(1)] + int(m.group(2))
        #Sort the thread list by total cpu 
        sorted_ThList = sorted(D.iteritems(), key=operator.itemgetter(1), reverse=True)
        #Get the top N threads
        topFiveTh = sorted_ThList[:cls.TopN]
        
        #Divide the total CPU usage of top N threads by the occurence of them in topdashH file 
        for t in topFiveTh:
            countMatches = len(re.findall('^%s.+$' % t[0], topHtxt, re.MULTILINE))
            s = str(hex(int(t[0])))[2:]
            cls.WildThreadList.append(WildThread(s.upper(), t[1] / countMatches))
        
        

    @classmethod
    def __RunsJava5(cls):
        if cls.JAVAPLATFORM[1].startswith('5.'):
            return True
    
    @classmethod
    def __RunsJava6(cls):
        if cls.JAVAPLATFORM[1].startswith('1.6'):
            return True

    
    @classmethod
    def FindThreadsDetails(cls):
        """Analyze the cores and find details(stack trace etc) about the top 5 CPU Consuming threads"""
        jclistl = glob.glob('./javacore.*')
                           
        for i in cls.WildThreadList:
            for jc in jclistl:
                #Select only the threads region of javacore file(JRE 5 or 1.6)
                thdumptxt = ""
                if cls.__RunsJava5():
                    for line in open(jc, 'r'):
                        if line.startswith('3XMTHREADINFO') or line.startswith('4XESTACKTRACE'):
                            thdumptxt += line
                elif cls.__RunsJava6():
                    inThreadArea = False
                    for line in open(jc, 'r'):
                        if line.startswith('1XMTHDINFO'):
                            inThreadArea = True
                        elif line.startswith('1SCLTEXTCRTW'):
                            inThreadArea = False
                            break
                        elif inThreadArea == True:
                            thdumptxt += line
                else:
                    raise UnknownJREException('Current JRE is not a known JRE...')
                    
                #Get the specific thread details (stack trace etc)
                if cls.__RunsJava5():
                    match = re.search('^.*3XMTHREADINFO(.+?native\sID\:0x[0]+%s.+?)3XMTHREADINFO.*$' % i.tid, thdumptxt, re.DOTALL)
                elif cls.__RunsJava6():
                    match = re.search('^.*NULL(.+?native\sthread\sID\:0x%s.+?)NULL.*$' % i.tid, thdumptxt, re.DOTALL)
                    
                if match != None:
                    i.details = match.group(1)
                    break
    
    @classmethod
    def FindJavaVersion(cls):
        """Find the JRE version and search for the appropriate patterns"""
        jclistl = glob.glob('./javacore.*')
        for line in open(jclistl[0], 'r'):
            if line.startswith('1CIJAVAVERSION'):
                match = re.search('^1CIJAVAVERSION\s+?(\w+)\s+?(\S+)\s+.+$', line)
                if match != None:
                    cls.JAVAPLATFORM.append(match.group(1))
                    cls.JAVAPLATFORM.append(match.group(2))

    @classmethod      
    def printThreadList(cls):
        """"Print the Contents of Thread list"""
        for i in cls.WildThreadList:
            i.printWildThread()

########################################################################################################
#class UnknownJREException
#Exception class that is raised when a not known JRE is found
########################################################################################################
class UnknownJREException(Exception):
    pass
        
########################################################################################################
#Main 
########################################################################################################     
if __name__ == '__main__':
    try:
        LinperfFileHandler.MoveJavacoresHere() 
        topHtxt = LinperfFileHandler.readtopDashH()
        ThreadAnalysis.FindJavaVersion()
        ThreadAnalysis.RateJavaThreadsPerformance(topHtxt)   
        ThreadAnalysis.FindThreadsDetails()
        ThreadAnalysis.printThreadList()
    except Exception as X:
        print X
        print 'Exiting...'
        sys.exit(1)
